<?php

namespace PhpYes\Com;

use PhpYes\Yes;

/**
 * 发现了比较通用的渲染逻辑，可以将逻辑写到这个类中来，
 * 但渲染逻辑不通用，而是和特定业务结合的紧密，请把渲染逻辑放到业务层，
 * 不要把和特定业务结合紧密的渲染逻辑写到这个类中
 *
 * 项目中可以使用其它 render, 只要确保在 Controller 中或者其它地方要渲染数据时能用就行
 *
 * Class Render
 * @package PhpYes\Com
 */
class Render
{
    /**
     * @var RenderDependsInterface
     */
    private $depends;

    /**
     * 是否使用 200 包装真正的 Response Status Code 来返回报文, default: false
     * 设计以下函数会根据这个字段来调整返回报文
     * returnJson
     * returnJsonWithErrorMessage
     * returnJsonSuccess
     * @var bool
     */
    public $ret200 = false;

    /**
     * 设计用于向 view 传递数据，
     * 服务于 渲染，不服务于 Render 类的实例，
     * 类似于 渲染函数(例如 display($data)) 的函数参数 $data，
     * setData 1 次 渲染 1 次，渲染函数 返回前会被清除，以免对下次渲染造成影响，
     * 假设整个渲染可以分多次合作构成，例如:
     * http_response_code(200);
     * header('Content-Type: text/html');
     * Render->d($data1)->displayComponent(Component1);
     * Render->displayComponent(Component2);
     * Render->d($data3)->displayComponent(Component3);
     * 渲染完 Component1 后如果没有清除 $data1，那么渲染 Component2 时就可以用到 $data1，
     * 长此这样做必定会造成混乱。
     *
     * 如果不设计成 Render 的成员变量 而 保持为 渲染函数 的函数参数 $data，
     * 那么在 view 中一不小心就将 $data 用作他用，改变 $data，
     * 另外，渲染函数 的函数参数 $data 只能在 渲染函数中有效，
     * 而 Render 的成员变量 可以在 Render 的所有成员函数中有效
     *
     * @var mixed
     */
    public $data;

    private $_header_cb; // before render callback

    // private $ar_cb; // after render callback 还未遇到使用场景

    /**
     * @param RenderDependsInterface $depends
     * @param bool $ret200 - 是否使用 200 包装真正的 Response Status Code 来返回报文, default: false
     */
    public function __construct(RenderDependsInterface $depends, $ret200 = false)
    {
        $this->depends = $depends;
        $this->ret200 = $ret200;
    }

    /**
     * setData, $this->data 是 public, 还设计一个 setter 函数是为了 链式调用
     * 例如: Render->d(Yes::$v)->display('/path/to/page.html.php');
     *
     * @param mixed $data
     * @return $this
     */
    public function d($data)
    {
        $this->data = $data;
        return $this;
    }

    /**
     * 设置一个在渲染之前调用的回调函数，主要用于设置额外的 header
     *
     * $callback 服务于 渲染，不服务于 Render 类的实例，
     * 类似于 渲染函数(例如 display($callback)) 的函数参数 $callback，
     * header_cb($callback) 设置 1 次 渲染 1 次，
     * 渲染函数 返回前会被清除，以免对下次渲染造成影响，
     * 其实在调用完 $callback 后就会立即清除 $callback 而不用等到 渲染函数 返回前
     *
     * 在渲染之前调用的 $callback 使用场景较少，
     * 所以设计成 Render 的 setter 成员函数，
     * 为了简化 渲染函数 的函数参数
     *
     * [RFC9110, Section 8.3. Content-Type] :
     * Although Content-Type is defined as a singleton field,
     * it is sometimes incorrectly generated multiple times,
     * resulting in a combined field value that appears to be a list.
     * Recipients often attempt to handle this error by using the last syntactically valid member of the list,
     * leading to potential interoperability and security issues if different implementations have different error handling behaviors.
     *
     * [RFC9111, Section 4.2.1. Calculating Freshness Lifetime] :
     * When there is more than one value present for a given directive
     * (e.g., two Expires header field lines or multiple Cache-Control: max-age directives),
     * either the first occurrence should be used or the response should be considered stale.
     * If directives conflict (e.g., both max-age and no-cache are present), the most restrictive directive should be honored.
     * Caches are encouraged to consider responses that have invalid freshness information
     * (e.g., a max-age directive with non-integer content) to be stale.
     *
     * 按照 rfc 的说法，应该把 header_cb() 放在 http_response_code(); header('Content-Type: '); 之后，
     * header('Cache-Control: '); 之前，以保证 header_cb() 中的 Content-Type 和 Cache-Control 的自定义设置 可以覆盖 默认设置，
     * 这样做是为了程序更加健壮，但是很明显 多次设置 Content-Type 和 Cache-Control 是不正确的(generating Content-Type or Cache-Control multiple times is incorrect),
     * 所以，禁止这样做，在知道发生了这样的问题后应该立即改正，可以选择直接渲染，不必调用这里的函数，本来也没几行代码，实现起来很简单
     *
     * @param callable $callback - callback() : void
     * @return $this
     */
    public function header_cb($callback)
    {
        $this->_header_cb = $callback;
        return $this;
    }

    /**
     * echo
     *
     * @param string $text
     * @param int $httpRspCode
     * @param string|null $contentType - empty($contentType) 则不会输出 header('Content-Type: ' . $contentType);
     * @return string
     */
    public function echoText($text = '', $httpRspCode = 200, $contentType = 'text/plain')
    {
        $text = empty($text) ? '' : trim($text);
        $this->data = $text;

        http_response_code($httpRspCode);
        $contentType = empty($contentType) ? null : trim($contentType);
        if ($contentType) {
            header('Content-Type: ' . $contentType);
        }
        if ($this->_header_cb) {
            $callback = $this->_header_cb;
            $callback();
            $this->_header_cb = null;
        }
        header('Cache-Control: no-cache');

        echo $text;

        return Yes::OK;
    }

    /**
     * 直接使用 php 渲染
     *
     * @param string $page - 页面的绝对路径
     * @param mixed $data
     * @param int $httpRspCode
     * @return string
     */
    public function display($page, $data = null, $httpRspCode = 200)
    {
        $this->data = $data;

        http_response_code($httpRspCode);
        header('Content-Type: text/html');
        if ($this->_header_cb) {
            $callback = $this->_header_cb;
            $callback();
            $this->_header_cb = null;
        }
        header('Cache-Control: no-cache');

        require $page;
        return Yes::OK;
    }

    /**
     * 使用 Smarty 渲染，简单封装一下，只是为了调用时少写几行代码，如果不能满足需求，请直接使用 Smarty
     *
     * @param string $page
     * @param mixed $data
     * @param int $httpRspCode
     * @throws \Exception
     */
    public function displayUseSmarty($page, $data = null, $httpRspCode = 200)
    {
        throw new \Exception('还没有实现');
    }

    /**
     * 使用 Twig 渲染，简单封装一下，只是为了调用时少写几行代码，如果不能满足需求，请直接使用 Twig
     *
     * @param string $page
     * @param mixed $data
     * @param int $httpRspCode
     * @throws \Exception
     */
    public function displayUseTwig($page, $data = null, $httpRspCode = 200)
    {
        throw new \Exception('还没有实现');
    }

    /**
     * 展示 Error
     * 调用之前应该区分出 请求是 页面请求 还是 XMLHttpRequest请求，XMLHttpRequest请求不应该调用这个函数
     * @param string $errMsg
     * @param int $httpRspCode
     * @return string
     */
    public function displayError($errMsg, $httpRspCode = 400)
    {
        $httpRspCode = intval($httpRspCode);
        if ($httpRspCode >= 500) {
            return $this->display5xx($errMsg, $httpRspCode);
        } elseif ($httpRspCode >= 400) {
            return $this->display4xx($errMsg, $httpRspCode);
        } else {
            return $this->display($this->depends->page_error(), array('code' => $httpRspCode, 'msg' => $errMsg), $httpRspCode);
        }
    }

    /**
     * 展示 5xx Server Error
     * 调用之前应该区分出 请求是 页面请求 还是 XMLHttpRequest请求，XMLHttpRequest请求不应该调用这个函数
     * @param string $errMsg
     * @param int $httpRspCode
     * @return string
     */
    public function display5xx($errMsg, $httpRspCode = 500)
    {
        return $this->display($this->depends->page_5xx(), array('code' => $httpRspCode, 'msg' => $errMsg), $httpRspCode);
    }

    /**
     * 展示 4xx Client Error
     * 调用之前应该区分出 请求是 页面请求 还是 XMLHttpRequest请求，XMLHttpRequest请求不应该调用这个函数
     * @param string $errMsg
     * @param int $httpRspCode
     * @return string
     */
    public function display4xx($errMsg, $httpRspCode = 400)
    {
        $httpRspCode = intval($httpRspCode);
        if ($httpRspCode == 404) {
            return $this->display404($errMsg);
        }

        return $this->display($this->depends->page_4xx(), array('code' => $httpRspCode, 'msg' => $errMsg), $httpRspCode);
    }

    /**
     * 展示 404
     * 调用之前应该区分出 请求是 页面请求 还是 XMLHttpRequest请求，XMLHttpRequest请求不应该调用这个函数
     * @param string $errMsg
     * @return string
     */
    public function display404($errMsg = '')
    {
        return $this->display($this->depends->page_404(), array('code' => 404, 'msg' => $errMsg), 404);
    }

    /**
     * @param array $data
     * @param int $httpRspCode
     * @param int $options - json_encode options
     * @return string
     */
    private function return_json($data, $httpRspCode = 200, $options = 0)
    {
        if (!is_array($data)) {
            throw new \InvalidArgumentException('Render::returnJson 参数错误');
        }

        $this->data = $data;

        http_response_code($httpRspCode);
        header('Content-Type: application/json');
        if ($this->_header_cb) {
            $callback = $this->_header_cb;
            $callback();
            $this->_header_cb = null;
        }
        header('Cache-Control: no-cache');

        echo json_encode($this->data, intval($options));

        return Yes::OK;
    }

    /**
     * @param array $data
     * @param int $httpRspCode
     * @param int $options - json_encode options
     * @return string
     */
    public function returnJson($data, $httpRspCode = 200, $options = 0)
    {
        if (empty($this->ret200)) {
            return $this->return_json($data, $httpRspCode, $options);
        } else {
            return $this->return_json(array('code' => $httpRspCode, 'data' => $data), 200, $options);
        }
    }

    /**
     * @param string $errMsg
     * @param int $httpRspCode
     * @param int $options - json_encode options
     * @return string
     */
    public function returnJsonError($errMsg, $httpRspCode = 400, $options = 0)
    {
        if (empty($this->ret200)) {
            return $this->return_json(array('msg' => $errMsg), $httpRspCode, $options);
        } else {
            return $this->return_json(array('code' => $httpRspCode, 'msg' => $errMsg), 200, $options);
        }
    }

    /**
     * @return string
     */
    public function returnJsonSuccess()
    {
        if (empty($this->ret200)) {
            return $this->return_json(array('msg' => 'success'), 200);
        } else {
            return $this->return_json(array('code' => 200, 'msg' => 'success'), 200);
        }
    }

    /**
     * @param string $callback
     * @param array $data
     * @param int $httpRspCode
     * @param int $options - json_encode options
     * @return string
     */
    public function returnJsonp($callback, $data, $httpRspCode = 200, $options = 0)
    {
        $callback = empty($callback) ? null : trim($callback);
        if (empty($callback) || !is_array($data)) {
            throw new \InvalidArgumentException('Render::returnJsonp 参数错误');
        }

        $this->data = $data;

        http_response_code($httpRspCode);
        header('Content-Type: text/javascript');
        if ($this->_header_cb) {
            $callback = $this->_header_cb;
            $callback();
            $this->_header_cb = null;
        }
        header('Cache-Control: no-cache');

        $ret = json_encode($this->data, intval($options));
        echo "{$callback}({$ret})";

        return Yes::OK;
    }

    /**
     * @param string $url
     * @param int $httpRspCode
     * @return string
     */
    public function redirect($url, $httpRspCode = 302)
    {
        $this->data = array('redirectUrl' => $url);

        $httpRspCode = intval($httpRspCode);
        if ($httpRspCode !== 302) {
            http_response_code($httpRspCode);
        }
        header('Content-Type: text/html');
        if ($this->_header_cb) {
            $callback = $this->_header_cb;
            $callback();
            $this->_header_cb = null;
        }
        header('Cache-Control: no-cache');

        // The "Location:" header is a special case. Not only does it send this header back to the browser, but it also returns a REDIRECT (302) status code to the browser unless the 201 or a 3xx status code has already been set.
        header('Location: ' . $url);
        echo
        sprintf('<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=%1$s" />

        <title>Redirecting to %1$s</title>
    </head>
    <body>
        Redirecting to <a href="%1$s">%1$s</a>.
    </body>
</html>', htmlspecialchars($url, ENT_QUOTES, 'UTF-8'));

        return Yes::OK;
    }

    /**
     * @param string $filename
     * @param array $data - refer CsvGenerator::gen($data)
     * @return string
     */
    public function downloadCsv($filename, $data)
    {
        if (!is_array($data)) {
            throw new \InvalidArgumentException('Render::downloadCsv 参数错误');
        }

        $this->data = $data;

        $filename = empty($filename) ? null : trim($filename);
        if (empty($filename)) {
            $filename = 'csv.csv';
        }
        $csv = CsvGenerator::gen($data);
        if (empty($csv)) {
            $csv = 'empty';
        }

        http_response_code(200);
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        if ($this->_header_cb) {
            $callback = $this->_header_cb;
            $callback();
            $this->_header_cb = null;
        }
        header('Cache-Control: no-cache');

        echo $csv;

        return Yes::OK;
    }

}